﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using Dexter.Caching;
using Dexter.Localization.Exception;
using Dexter.Storage;

namespace Dexter.Localization.Po {
	public class LocalizationProvider : ILocalizationProvider {
		static readonly Dictionary <char , char> escapeTranslations = new Dictionary <char , char> {{'n' , '\n'} , {'r' , '\r'} , {'t' , '\t'}};
		readonly ICacheProvider cacheProvider;
		readonly IStorageProvider storage;

		/// <summary>
		/// 	Initializes a new instance of the <see cref = "T:System.Object" /> class.
		/// </summary>
		/// <param name = "storage">The storage.</param>
		/// <param name = "cacheProvider">The cache provider.</param>
		public LocalizationProvider ( IStorageProvider storage , ICacheProvider cacheProvider ) {
			this.storage = storage;
			this.cacheProvider = cacheProvider;
		}

		protected string ModulesLocalizationFilePathFormat {
			get {
				return IsHostedInAspnet ( )
				       	? "App_Data/Localization/{0}/{1}.po"
				       	: "Localization/{0}/{1}.po";
			}
		}

		#region ILocalizationProvider Members

		/// <summary>
		/// Gets all localized string for specified scope.
		/// </summary>
		/// <param name="scope">The scope.</param>
		/// <param name="cultureName">Name of the culture.</param>
		/// <returns></returns>
		/// <exception cref="ArgumentNullException">Will be throw if <paramref name="scope"/> is null.</exception>
		/// <exception cref="ArgumentException">Will be throw if <paramref name="scope"/> is empty.</exception>
		/// <exception cref="ArgumentNullException">Will be throw if <paramref name="cultureName"/> is null.</exception>
		/// <exception cref="ArgumentException">Will be throw if <paramref name="cultureName"/> is empty.</exception>
		public IEnumerable <LocalizedString> GetAllLocalizedStringForSpecifiedScope ( string scope , string cultureName ) {
			return GetAllLocalizedStringForSpecifiedScope ( null , scope , cultureName );
		}

		/// <summary>
		/// Gets all localized string for specified scope.
		/// </summary>
		/// <param name="moduleName">Name of the module.</param>
		/// <param name="scope">The scope.</param>
		/// <param name="cultureName">Name of the culture.</param>
		/// <returns></returns>
		/// <exception cref="ArgumentNullException">Will be throw if <paramref name="scope"/> is null.</exception>
		/// <exception cref="ArgumentException">Will be throw if <paramref name="scope"/> is empty.</exception>
		/// <exception cref="ArgumentNullException">Will be throw if <paramref name="cultureName"/> is null.</exception>
		/// <exception cref="ArgumentException">Will be throw if <paramref name="cultureName"/> is empty.</exception>
		public IEnumerable <LocalizedString> GetAllLocalizedStringForSpecifiedScope ( string moduleName , string scope , string cultureName ) {
			if ( scope == string.Empty ) {
				throw new ArgumentException ( );
			}

			if ( scope == null ) {
				throw new ArgumentNullException ( );
			}

			if ( cultureName == string.Empty ) {
				throw new ArgumentException ( );
			}

			if ( cultureName == null ) {
				throw new ArgumentNullException ( );
			}

			if ( string.IsNullOrEmpty ( moduleName ) ) {
				moduleName = "core";
			}

			var culture = new CultureInfo ( cultureName );

			CultureDictionary cultureDictionary = LoadCulture ( moduleName , culture );

			string scopedKey = ( scope + "|" ).ToLowerInvariant ( );

			IEnumerable <LocalizedString> result = cultureDictionary.Translations.Where ( x => x.Key.StartsWith ( scopedKey ) ).Select ( x => new LocalizedString ( x.Value , scope , x.Key.Replace ( scopedKey , string.Empty ) , null ) );

			return result;
		}

		/// <summary>
		/// Gets the localized string.
		/// </summary>
		/// <param name="msgId">The MSG id.</param>
		/// <param name="cultureName">Name of the culture.</param>
		/// <returns>The result will never be null.</returns>
		/// <exception cref="ArgumentNullException">Will be throw if <paramref name="msgId"/> is null.</exception>
		/// <exception cref="ArgumentException">Will be throw if <paramref name="msgId"/> is empty.</exception>
		/// <exception cref="ArgumentNullException">Will be throw if <paramref name="cultureName"/> is null.</exception>
		/// <exception cref="ArgumentException">Will be throw if <paramref name="cultureName"/> is empty.</exception>
		public LocalizedString GetLocalizedString ( string msgId , string cultureName ) {
			return GetLocalizedString ( null , msgId , cultureName );
		}

		/// <summary>
		/// Gets the localized string.
		/// </summary>
		/// <param name="moduleName">Name of the module.</param>
		/// <param name="msgId">The MSG id.</param>
		/// <param name="cultureName">Name of the culture.</param>
		/// <returns>The result will never be null.</returns>
		/// <exception cref="ArgumentNullException">Will be throw if <paramref name="msgId"/> is null.</exception>
		/// <exception cref="ArgumentException">Will be throw if <paramref name="msgId"/> is empty.</exception>
		/// <exception cref="ArgumentNullException">Will be throw if <paramref name="cultureName"/> is null.</exception>
		/// <exception cref="ArgumentException">Will be throw if <paramref name="cultureName"/> is empty.</exception>
		public LocalizedString GetLocalizedString ( string moduleName , string msgId , string cultureName ) {
			return GetLocalizedString ( moduleName , null , msgId , cultureName );
		}

		/// <summary>
		/// Gets the localized string.
		/// </summary>
		/// <param name="moduleName">Name of the module.</param>
		/// <param name="scope">The scope.</param>
		/// <param name="msgId">The MSG id.</param>
		/// <param name="cultureName">Name of the culture.</param>
		/// <returns>The result will never be null.</returns>
		/// <exception cref="ArgumentNullException">Will be throw if <paramref name="msgId"/> is null.</exception>
		/// <exception cref="ArgumentException">Will be throw if <paramref name="msgId"/> is empty.</exception>
		/// <exception cref="ArgumentNullException">Will be throw if <paramref name="cultureName"/> is null.</exception>
		/// <exception cref="ArgumentException">Will be throw if <paramref name="cultureName"/> is empty.</exception>
		public LocalizedString GetLocalizedString ( string moduleName , string scope , string msgId , string cultureName ) {
			return GetLocalizedString ( moduleName , scope , msgId , cultureName , null );
		}

		/// <summary>
		/// Gets the localized string.
		/// </summary>
		/// <param name="moduleName">Name of the module.</param>
		/// <param name="scope">The scope.</param>
		/// <param name="msgId">The MSG id.</param>
		/// <param name="cultureName">Name of the culture.</param>
		/// <param name="args">The args.</param>
		/// <returns>The result will never be null.</returns>
		/// <exception cref="ArgumentNullException">Will be throw if <paramref name="msgId"/> is null.</exception>
		/// <exception cref="ArgumentException">Will be throw if <paramref name="msgId"/> is empty.</exception>
		/// <exception cref="ArgumentNullException">Will be throw if <paramref name="cultureName"/> is null.</exception>
		/// <exception cref="ArgumentException">Will be throw if <paramref name="cultureName"/> is empty.</exception>
		public LocalizedString GetLocalizedString ( string moduleName , string scope , string msgId , string cultureName , params object[] args ) {
			if ( msgId == string.Empty ) {
				throw new ArgumentException ( );
			}

			if ( msgId == null ) {
				throw new ArgumentNullException ( );
			}

			if ( cultureName == string.Empty ) {
				throw new ArgumentException ( );
			}

			if ( cultureName == null ) {
				throw new ArgumentNullException ( );
			}

			if ( string.IsNullOrEmpty ( moduleName ) ) {
				moduleName = "core";
			}

			var culture = new CultureInfo ( cultureName );

			var cultureDictionary = LoadCulture ( moduleName , culture );

			string scopedKey = ( scope + "|" + msgId ).ToLowerInvariant ( );

			if ( cultureDictionary.Translations == null ) {
				return new LocalizedString ( msgId , scope , msgId , args );
			}

			if ( cultureDictionary.Translations.ContainsKey ( scopedKey ) ) {
				var r = new LocalizedString ( cultureDictionary.Translations[ scopedKey ] , scope , msgId , args );

				return r;
			}

			string genericKey = ( "|" + msgId ).ToLowerInvariant ( );
			if ( cultureDictionary.Translations.ContainsKey ( genericKey ) ) {
				var r = new LocalizedString ( cultureDictionary.Translations[ genericKey ] , scope , msgId , args );

				return r;
			}

			return null;
		}

		/// <summary>
		/// Saves the module.
		/// </summary>
		/// <param name="cultureName">Name of the culture.</param>
		/// <param name="moduleName">Name of the module.</param>
		/// <param name="inputStream">The input stream.</param>
		/// <exception cref="LocalizationModuleExistentException">Will be throw if there is an existing module with the same specified name for the same specified culture.</exception>
		/// <exception cref="ArgumentNullException">Will be throw if <paramref name="moduleName"/> is null.</exception>
		/// <exception cref="ArgumentException">Will be throw if <paramref name="moduleName"/> is empty.</exception>
		/// <exception cref="ArgumentNullException">Will be throw if <paramref name="cultureName"/> is null.</exception>
		/// <exception cref="ArgumentException">Will be throw if <paramref name="cultureName"/> is empty.</exception>
		public void SaveModule ( string cultureName , string moduleName, Stream inputStream ){
			Save ( moduleName, cultureName, inputStream, true );
		}

		/// <summary>
		/// Saves the or update module.
		/// </summary>
		/// <param name="cultureName">Name of the culture.</param>
		/// <param name="moduleName">Name of the module.</param>
		/// <param name="inputStream">The input stream.</param>
		/// <exception cref="ArgumentNullException">Will be throw if <paramref name="moduleName"/> is null.</exception>
		/// <exception cref="ArgumentException">Will be throw if <paramref name="moduleName"/> is empty.</exception>
		/// <exception cref="ArgumentNullException">Will be throw if <paramref name="cultureName"/> is null.</exception>
		/// <exception cref="ArgumentException">Will be throw if <paramref name="cultureName"/> is empty.</exception>
		public void SaveOrUpdateModule ( string cultureName , string moduleName, Stream inputStream ) {
			Save ( moduleName , cultureName , inputStream, false );
		}

		/// <summary>
		/// 	Deletes the module.
		/// </summary>
		/// <param name = "cultureName">Name of the culture.</param>
		/// <param name = "moduleName">Name of the module.</param>
		/// <exception cref = "ArgumentException">Will be throw if there is an existing module with the same specified name for the same specified culture.</exception>
		/// <exception cref = "ArgumentNullException">Will be throw if <paramref name = "moduleName" /> is null.</exception>
		/// <exception cref = "ArgumentException">Will be throw if <paramref name = "moduleName" /> is empty.</exception>
		/// <exception cref = "ArgumentNullException">Will be throw if <paramref name = "cultureName" /> is null.</exception>
		/// <exception cref = "ArgumentException">Will be throw if <paramref name = "cultureName" /> is empty.</exception>
		/// <exception cref = "LocalizationModuleNotFoundException">Will be throw if there isn't an existing module with the same specified name for the same specified culture.</exception>
		public void DeleteModule ( string cultureName , string moduleName ) {
			if (moduleName == string.Empty) {
				throw new ArgumentException ( );
			}

			if (moduleName == null) {
				throw new ArgumentNullException ( );
			}

			if (cultureName == string.Empty) {
				throw new ArgumentException ( );
			}

			if (cultureName == null) {
				throw new ArgumentNullException ( );
			}
			
			var filePath = string.Format ( ModulesLocalizationFilePathFormat, cultureName, moduleName );

			if (storage.FileExist ( filePath )) {
				storage.DeleteFile ( filePath );
			}
			else {
				throw new LocalizationModuleNotFoundException("Unable to find the specified localization module");
			}
		}

		#endregion

		void Save ( string moduleName, string cultureName, Stream inputStream, bool throwExceptionIfFileExist ) {
			if (moduleName == string.Empty) {
				throw new ArgumentException ( );
			}

			if (moduleName == null) {
				throw new ArgumentNullException ( );
			}

			if (cultureName == string.Empty) {
				throw new ArgumentException ( );
			}

			if (cultureName == null) {
				throw new ArgumentNullException ( );
			}

			if (inputStream == null) {
				throw new ArgumentNullException ( );
			}

			var filePath = string.Format ( ModulesLocalizationFilePathFormat, cultureName, moduleName );

			if (storage.FileExist ( filePath )) {
				if (throwExceptionIfFileExist) {
					throw new LocalizationModuleExistentException ( "Another localization module with the same name and the same culture exists into the repository." );
				}

				storage.DeleteFile ( filePath );
			}

			storage.SaveStream ( filePath, inputStream );
		}

		CultureDictionary LoadCulture ( string moduleName , CultureInfo culture ) {
			const string REGION_NAME = "Localization";
			string key = string.Format ( "Dictionary.Culture:{0}.Module{1}" , culture.Name , moduleName );

			var value = cacheProvider.Get <CultureDictionary> ( key , REGION_NAME )
				.Return ( o => o ,
				          ( ) => {
				          	var c = new CultureDictionary ( ) {
				          		Culture = culture ,
				          		Translations = LoadTranslations ( moduleName , culture )
				          	};

				          	cacheProvider.Put ( key , c , TimeSpan.FromDays ( 1 ) , REGION_NAME , new[] {"Localization" , culture.Name} );

				          	return c;
				          } );

			return value;
		}

		IDictionary <string , string> LoadTranslations ( string moduleName , CultureInfo culture ) {
			var filePath = string.Format ( ModulesLocalizationFilePathFormat , culture.Name , moduleName );
			var file = storage.GetFile ( filePath );

			if(file == null)
				return null;

			using ( var stream = file.OpenRead ( ) ) {
				using ( var streamReader = new StreamReader ( stream ) ) {
					string container = streamReader.ReadToEnd ( );

					IDictionary <string , string> result = new Dictionary <string , string> ( );

					ParseLocalizationStream ( container , result );
					return result;
				}
			}
		}

		static void ParseLocalizationStream ( string text , IDictionary <string , string> translations ) {
			StringReader reader = new StringReader ( text );
			string poLine , scope;

			string id = scope = String.Empty;

			while ( ( poLine = reader.ReadLine ( ) ) != null ) {
				if ( poLine.StartsWith ( "#:" ) ) {
					scope = ParseScope ( poLine );
					continue;
				}

				if ( poLine.StartsWith ( "msgid" ) ) {
					id = ParseId ( poLine );
					continue;
				}

				if ( poLine.StartsWith ( "msgstr" ) ) {
					string translation = ParseTranslation ( poLine );
					// ignore incomplete localizations (empty msgid or msgstr)
					if ( !String.IsNullOrWhiteSpace ( id ) && !String.IsNullOrWhiteSpace ( translation ) ) {
						string scopedKey = ( scope + "|" + id ).ToLowerInvariant ( );
						translations.Add ( scopedKey , translation );
					}
					id = scope = String.Empty;
				}
			}
		}

		static string ParseTranslation ( string poLine ) {
			return Unescape ( poLine.Substring ( 6 ).Trim ( ).Trim ( '"' ) );
		}

		static string ParseId ( string poLine ) {
			return Unescape ( poLine.Substring ( 5 ).Trim ( ).Trim ( '"' ) );
		}

		static string ParseScope ( string poLine ) {
			return Unescape ( poLine.Substring ( 2 ).Trim ( ).Trim ( '"' ) );
		}

		static string Unescape ( string str ) {
			StringBuilder sb = null;
			bool escaped = false;
			for ( var i = 0; i < str.Length; i++ ) {
				var c = str[ i ];
				if ( escaped ) {
					if ( sb == null ) {
						sb = new StringBuilder ( str.Length );
						if ( i > 1 ) {
							sb.Append ( str.Substring ( 0 , i - 1 ) );
						}
					}
					char unescaped;
					sb.Append ( escapeTranslations.TryGetValue ( c , out unescaped )
					            	? unescaped
					            	: c );
					escaped = false;
				}
				else {
					if ( c == '\\' ) {
						escaped = true;
					}
					else if ( sb != null ) {
						sb.Append ( c );
					}
				}
			}
			return sb == null
			       	? str
			       	: sb.ToString ( );
		}

		static bool IsHostedInAspnet ( ) {
			return ( AppDomain.CurrentDomain.GetData ( ".appDomain" ) != null );
		}
	}
}